/*
 * Tf: a unit test framework for C
 * Copyright (C) 2012 zrezek at gmail.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#include "tf.h"
#include "tf-private.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

struct state tf_state;

/////////////////////////////////////////////////////////////////////
// forwards
void tf_invoke_test_remote(struct tf_fixture* fixture, struct tf_test* test);

/////////////////////////////////////////////////////////////////////
// List manipulations
static void init_list(struct tf_list *list)
{
    assert(list);

    list->first = (struct tf_node*) &list->dummy;
    list->dummy = 0;
    list->last = (struct tf_node*) &list->first; 
}

static void add_tail(struct tf_list *list, struct tf_node* node)
{
    struct tf_node* ln;

    assert(list && node);

    ln = list->last;
    node->next = ln->next;
    ln->next = node;
    node->pred = ln;
    list->last = node;
}

static struct tf_node* rem_head(struct tf_list* list)
{
    struct tf_node* n;

    assert(list);

    n = list->first;

    if (n->next)
    {
        list->first = n->next;
        n->next->pred = n->pred;
        return n;
    }
    return 0;
}

static struct tf_node* rem_tail(struct tf_list* list)
{
    struct tf_node* n, *p;

    assert(list);

    n = list->last;
    p = n->pred;

    if (p)
    {
        p->next = (struct tf_node*) &list->dummy;
        list->last = p;
        return n;
    }
    return 0;
}

static struct tf_node* rem_node(struct tf_node *n)
{
    assert(n);

    n->next->pred = n->pred;
    n->pred->next = n->next;
    return n;
}

static void insert_before(struct tf_node* n, struct tf_node* sn)
{
    struct tf_node* pn;

    assert(n && sn);

    pn = sn->pred;

    n->next = sn;
    n->pred = pn;
    pn->next = sn->pred = n;
}

static void insert_behind(struct tf_node* pn, struct tf_node* n)
{
    struct tf_node* sn;

    assert(pn && n);
 
    sn = pn->next;

    n->next = sn;
    n->pred = pn;
    pn->next = sn->pred = n;
}

/////////////////////////////////////////////////////////////////////
// initialize global state
static void init_state()
{
    // *note* cannot touch the tf_state.fixtures here
    // as this field is initialized by fixture registration
    // callbacks that are run before main()

    init_list(&tf_state.fixtures_to_run);

    tf_state.test_execute_mode = TEST_EXECUTE_FORK;
    tf_state.mode = MODE_DRIVER;
}

/////////////////////////////////////////////////////////////////////
// initialize tests list in each fixture and call init function
static void init_fixtures()
{
    struct tf_fixture* fixture = (struct tf_fixture*) tf_state.fixtures.first;

    while (fixture->node.next)
    {
        assert(fixture->init);

        init_list(&fixture->tests);
        fixture->init(fixture);
    
        fixture = (struct tf_fixture*) fixture->node.next;
    }
}

/////////////////////////////////////////////////////////////////////
// 
static void dump()
{
    struct tf_fixture* fixture = (struct tf_fixture*) tf_state.fixtures.first;

    while (fixture->node.next)
    {
        struct tf_test* test = (struct tf_test*) fixture->tests.first;

        printf("%s:\n", fixture->path_and_name);
        printf("\tsetup function: 0x%x\n", fixture->setup_function);
        printf("\tteardown function: 0x%x\n",fixture->teardown_function);

        while (test->node.next)
        {
            printf("\ttest: %s\n", test->name);

            test = (struct tf_test*) test->node.next;
        }
        fixture = (struct tf_fixture*) fixture->node.next;
    }
}

static void check_errors()
{
    struct tf_fixture* fixture = (struct tf_fixture*) tf_state.fixtures.first;

    while (fixture->node.next)
    {    
        struct tf_test* test = (struct tf_test*) fixture->tests.first;

        while (test->node.next)
        {
            if (test->flags & TF_FLAG_ADDED_MULTIPLE)
                printf("Cannot add test '%s' to multiple fixtures.\n", test->name);
            test = (struct tf_test*) test->node.next;
        }
        fixture = (struct tf_fixture*) fixture->node.next;
    }
}

static int qsort_compare(const void * a, const void * b)
{
    struct tf_fixture** a_ptr = (struct tf_fixture**) a;
    struct tf_fixture** b_ptr = (struct tf_fixture**) b;

    return strcmp((*a_ptr)->path_and_name, (*b_ptr)->path_and_name);
}

static void sort_fixtures()
{
    struct tf_fixture** buffer = (struct tf_fixture**) 
        malloc(tf_state.fixtures_num * sizeof(struct tf_fixture*));
    struct tf_fixture* fixture;
    int index = 0;

    // remove all fixtures from list and put them into
    // qsort buffer
    while ((fixture = (struct tf_fixture*) rem_head(&tf_state.fixtures)))
    {
        buffer[index++] = fixture;
    }

    // sort fixtures
    qsort(buffer, tf_state.fixtures_num, sizeof(struct tf_fixture*), qsort_compare);
    
    // add fixtures back to the list in alphabetical order
    for (index = 0; index < tf_state.fixtures_num; index++)
    {
        add_tail(&tf_state.fixtures, (struct tf_node*) buffer[index]);
    }

    free(buffer);
}

static void gather_fixtures(const char* fixture_path)
{
    int path_len = fixture_path ? strlen(fixture_path) : 0;
    struct tf_fixture* fixture = (struct tf_fixture*) tf_state.fixtures.first;
    struct tf_fixture* next;

    while ((next = (struct tf_fixture*) fixture->node.next))
    {    
        if (!strncmp(fixture->path_and_name, fixture_path, path_len))
        {
            rem_node((struct tf_node*) fixture);
            add_tail(&tf_state.fixtures_to_run, (struct tf_node*) fixture); 
        }
        fixture = next;
    }
}

void tf_invoke_test_local(struct tf_fixture* fixture, struct tf_test* test)
{
    assert(fixture && test);
    assert(test->test_function);

    tf_state.condition = tf_state.file = 0;
    tf_state.line = -1;

    if (!setjmp(tf_state.fail_jmp_buf))
    {
        // setup fixture and ivoke test function
        if (fixture->setup_function)
            fixture->setup_function();
        test->test_function();
        printf("ok\n");
    }
    else
    {
        // test failed
        if (tf_state.condition)
        {
            printf("condition '%s' failed in file %s line %d\n",
                                        tf_state.condition,
                                        tf_state.file,
                                        tf_state.line);
        }
        else
        {
            printf("failed in file %s line %d\n",
                                        tf_state.condition,
                                        tf_state.file,
                                        tf_state.line);
        }
    }

    // in case there is any failure checking
    // code in teardown function setup jump environment
    // once again so we can avoid infinite loop
    // (execution would reach setjmp handler above)
    if (!setjmp(tf_state.fail_jmp_buf))
    {
        if (fixture->teardown_function)
            fixture->teardown_function();
    }
}

static void run_fixtures()
{
    struct tf_fixture* fixture = (struct tf_fixture*) tf_state.fixtures_to_run.first;

    while (fixture->node.next)
    {
        struct tf_test* test = (struct tf_test*) fixture->tests.first;

        printf("Running fixture '%s'\n", fixture->path_and_name);

        while (test->node.next)
        {
            printf("  Running test: '%s' ", test->name);

            if (tf_state.test_execute_mode == TEST_EXECUTE_LOCAL)
            {
                tf_invoke_test_local(fixture, test);
            }
            else if (tf_state.test_execute_mode == TEST_EXECUTE_FORK)
            {
                tf_invoke_test_remote(fixture, test);
            }
            else
                assert(0);
                
            test = (struct tf_test*) test->node.next;
        }
        fixture = (struct tf_fixture*) fixture->node.next;
    }
}

/////////////////////////////////////////////////////////////////////
// 
static void parse_cmd_line(int argc, char* argv[])
{
    int i;

    tf_state.executable_name = argv[0];

    for (i = 1; i < argc;)
    {
        if (!strcmp(argv[i], "--invoke"))
        {
            // next to arguments are fixture name and test name
            tf_state.mode = MODE_RUNNER;
            assert(i + 2 < argc);

            tf_state.fixture_to_execute = argv[i + 1];
            tf_state.test_to_execute = argv[i + 2];

            i += 2;
        }
        else
            i++;
    }
}

/////////////////////////////////////////////////////////////////////
// public apis declared in c-unit.h
void tf_register_fixture(struct tf_fixture* fixture)
{
    assert(fixture);

    if (!tf_state.fixtures.first)
        init_list(&tf_state.fixtures);

    add_tail(&tf_state.fixtures, (struct tf_node*) fixture);
    tf_state.fixtures_num++;
}

void tf_add_test_item(struct tf_fixture* fixture, struct tf_test* item)
{
    assert(fixture);

    if (!item)
    {
        printf("Cannot't add NULL as a test to fixture '%s'.\n",
                fixture->path_and_name);
        return;
    }

    if (item->flags & TF_FLAG_ADDED)
    {
        item->flags |= TF_FLAG_ADDED_MULTIPLE;
    }
    else
    {
        add_tail(&fixture->tests, (struct tf_node*) item);
        item->flags |= TF_FLAG_ADDED;
    }
}

void tf_set_setup_func(struct tf_fixture* fixture, void (*func)())
{
    assert(fixture);

    if (!func)
    {
        printf("Can't set NULL as a setup to fixture '%s'.\n",
                fixture->path_and_name);
    }
    else if (fixture->setup_function)
    {
        printf("Can't set setup function to fixture '%s' twice.\n",
                fixture->path_and_name);
    }
    else
    {
        fixture->setup_function = func;
    }
}

void tf_set_teardown_func(struct tf_fixture* fixture, void (*func)())
{
    assert(fixture);

    if (!func)
    {
        printf("Cannot't set NULL as a teardown to fixture '%s'.\n",
                fixture->path_and_name);
    }
    else if (fixture->teardown_function)
    {
        printf("Can't set teardown function to fixture '%s' twice.\n",
                fixture->path_and_name);
    }
    else
    {
        fixture->teardown_function = func;
    }
}

void tf_require(int condition_value, const char* condition_txt, const char* file, int line)
{
    // if condition evaluates to the positive value
    // than everything is fine
    if (condition_value)
        return;

    // condition evaluated to zero, stop execution
    // store failure data and go back to invocation place
    tf_state.condition = condition_txt;
    tf_state.file = file;
    tf_state.line = line;

    longjmp(tf_state.fail_jmp_buf, 1);
}

void tf_set_fixture_timeout(struct tf_fixture* fixture, int timeout)
{
    assert(fixture && timeout >= 0);

    fixture->timeout = timeout;
}

/////////////////////////////////////////////////////////////////////
// entry point
int main(int argc, char *argv[])
{
    init_state();
    init_fixtures();
    parse_cmd_line(argc, argv);

    if (tf_state.mode == MODE_DRIVER)
    {
        int i, fixtures_to_run = 0;
    
        check_errors();
        sort_fixtures();

        for (i = 1; i < argc; i++)
        {
            if (argv[i][0] != '-')
            {
                gather_fixtures(argv[i]);
                fixtures_to_run++;
            }
        }

        if (fixtures_to_run == 0)
        {
            // if no fixture was selected by user at the command line
            // add all fixtures to the run list
            gather_fixtures("");
        }

        run_fixtures();
    }
    else
    {
        tf_execute_test_remote();
    }
    return 0;
}
